	package scenetracer; 

import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

import scenetracer.core.Camera;
import scenetracer.core.Primitive;
import scenetracer.core.Ray;
import scenetracer.core.Scene;
import scenetracer.core.light.Light;
import scenetracer.core.light.PointLight;
import scenetracer.core.primitive.IntersectionState;
import scenetracer.core.primitive.Sphere;
import scenetracer.core.primitive.IntersectionState.IntersectionKind;
import scenetracer.core.shader.Shader;
import scenetracer.image.Color;
import scenetracer.math.Point3;
import scenetracer.math.Vector3;

public class SceneTracer { 
	public static final int DEPTH = 7; 
	private static BufferedImage screenThreads;
	private static final float DELTA = 0.001f;
	private static final int LINES = 50;
	private static GlobalCount gc;

	private boolean showProgress;
//	public KDTree tree;
	int threads;
	
	public boolean mustShowProgress() {
		return showProgress;
	}


	public void setShowProgress(boolean showProgress) {
		this.showProgress = showProgress;
	}

	private static Scene scene;

	public BufferedImage render(Scene scene, int nThreads){
		this.scene = scene;
		boolean doAntialiasing = scene.getAntialiasingMinValue()>0 || scene.getAntialiasingMaxValue()>0;

		if(nThreads==1)
			return renderCore(scene, doAntialiasing);
		else if(nThreads > 1)
			return renderCoreThread(doAntialiasing, nThreads);
		else{
			System.out.println("Wrong input of threads. Should be >= 1");
			System.exit(1);
			return null;
		}
	} 


	public Scene getScene() {
		return scene;
	}
	
	


	public boolean lightObs(Scene scene, Ray ray, Light light,
			Primitive primitive){
		IntersectionState currentIntersection = new IntersectionState(); 
		float minDistance = Float.MAX_VALUE; 
		float currentDistance;        
		Primitive minDistancePrimitive = null; 
		Point3 rayOrigin = ray.getOrigin();
		/*Triangle_aux tri = null, triInt = null;
		
		if(primitive instanceof TriangleMesh_aux){
			tri = ((TriangleMesh_aux)primitive).getTriOwner();
		}*/
		
		

		//currentIntersection.clearPoints();
		
		for (Primitive currentPrimitive : scene.getPrimitives()) {
			if(currentPrimitive == primitive)
				continue;

			
			if(currentPrimitive.Intersect(ray, currentIntersection)) 
			{
				/*if(currentPrimitive instanceof TriangleMesh_aux){
					triInt = ((TriangleMesh_aux)currentIntersection.getPrimitive()).getTriOwner();
					if(tri != triInt){
						currentDistance = currentIntersection.getPoints().get(0).distanceTo(rayOrigin);
						
						if(currentDistance < minDistance){                	
							minDistance = currentDistance; 
							minDistancePrimitive = currentPrimitive;
						}	
					}
				}
				else*/{
					currentDistance = currentIntersection.getIntersectionPoint().distanceTo(rayOrigin);
					
					if(currentDistance < minDistance && currentDistance > DELTA){                	
						minDistance = currentDistance; 
						minDistancePrimitive = currentPrimitive;
					}	
				}
			}
		}                         
        	
	
		return minDistancePrimitive != light;
	}
	
	public boolean extraLightObs(Scene scene, Ray ray, Light light,
			Primitive primitive){
		IntersectionState currentIntersection = new IntersectionState(); 
		float minDistance = Float.MAX_VALUE; 
		float currentDistance;        
		Primitive minDistancePrimitive = null; 
		Point3 rayOrigin = ray.getOrigin();
		/*LinkedList<Primitive> primitivesA = new LinkedList<Primitive>();
		primitivesA.addAll(scene.getPrimitives());
		primitivesA.add(light);*/
		
		for (Primitive currentPrimitive : scene.getPrimitives()) {
			if(currentPrimitive == primitive)
				continue;
			
			if(currentPrimitive.Intersect(ray, currentIntersection)) 
			{
				{
					currentDistance = currentIntersection.getIntersectionPoint().distanceTo(rayOrigin);
					
					if(currentDistance < minDistance && currentDistance > DELTA){            	
						minDistance = currentDistance; 
						minDistancePrimitive = currentPrimitive;
						
					}	
				}
			}
			
		}

		if(light.Intersect(ray, currentIntersection)) 
		{
			{
				currentDistance = currentIntersection.getIntersectionPoint().distanceTo(rayOrigin);
				if(currentDistance < minDistance && currentDistance > DELTA){ 
					minDistance = currentDistance; 
					minDistancePrimitive = light;

				}	
			}
		}

		return minDistancePrimitive != light;
	}


	private BufferedImage renderCore(Scene scene2, boolean doAntialiasing) {
		Camera camera = scene.getCamera();
		
		camera.update();
		
		Ray ray = null;
		Random random = new Random();

		int width = scene.getWidth(); 
		int height = scene.getHeight(); 
		int n = width * height;

		BufferedImage screen = new BufferedImage(width, height,BufferedImage.TYPE_3BYTE_BGR);

		int antialiasing = Math.max(scene.getAntialiasingMinValue(), scene.getAntialiasingMaxValue());
		int dim =  (int)Math.pow(2, antialiasing);
		float weight = 1f / (dim * dim); 
		float delta = 1f / dim;
		int cont=0;
		Color color = new Color(Color.BLACK);
		Color resultColor = new Color(Color.BLACK);
		float topX = 0, topY = 0, posX = 0, posY = 0;
		
		if (mustShowProgress()){
			System.out.println("|--------------------PROGRESS----------------------|");
			System.out.printf("|");
		}
		List<Light> lights = new ArrayList<Light>(); 
		lights.addAll(scene.getLights());
		

		for(int x = 0; x < width; x++){
			for(int y = 0; y < height; y++){
				
				if(doAntialiasing) {
					resultColor.set(Color.BLACK);
					topX = x - dim/2; 
					topY = y + dim/2;
					
					for(int i = 0;  i < dim; i++){
						for(int j=0; j < dim; j++){						
							posX = random.nextFloat()* delta + topX + delta*i;
							posY = random.nextFloat()* delta + topY + delta*j;
							ray = camera.getRay(posX, posY, width, height);                
							color.set(Color.BLACK);
							rayTrace(scene, ray, color, DEPTH);					
							resultColor.add(color);						
						}					
					}
					
					resultColor.mul(weight);
					screen.setRGB(x, height -1 - y, resultColor.toRGB());
				}
				else {
					ray = camera.getRay(x, y, width, height);                
					color.set(Color.BLACK);
					rayTrace(scene, ray, color, DEPTH);					
						
					screen.setRGB(x, height -1 - y, color.toRGB());
				}

				if (mustShowProgress() && ((height * x + y) % (n / 50) == 0)) {
					System.out.printf("#");
				}
			}             
		}       
		if(mustShowProgress())
			System.out.println("|  >>> COMPLETE!\n|--------------------------------------------------|");
		else
			System.out.println("COMPLETE!");
		
		return screen;  
	}
	
	private void renderCoreFromTo(boolean doAntialiasing, int line, Camera camera,
			int width, int height, List<Light> lights) {
		int from, to;
		
		
		
		
//		No se donde se tiene que crear por eso lo puse tanto aca como en el anterior
//		tree= new KDTree((ArrayList<Primitive>) scene.getPrimitives());
//		tree.build();
		Ray ray = null;
		

		
		//BufferedImage screenThreads = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
 
		
		
//		for (int hj=0; hj< lights.size() ; hj++){
//			System.out.println(scene.getLights().get(hj).toString() + " -- " + lights.get(hj).toString());
//			movePointLights(lights,scene.getLights() );
//			System.out.println(scene.getLights().get(hj).toString() + " -- " + lights.get(hj).toString());
//		}
		List<Light> lightsAux = new ArrayList<Light>(); 
		lightsAux.addAll(lights);
		
		Color color = new Color(Color.BLACK);
		Color resultColor = new Color(Color.BLACK);
		
		from = line*(height/LINES);
		if(line == LINES -1 )
			to = height;
		else
			to = (line+1)*(height/LINES);

		int x,y;
		if(doAntialiasing) {
			float topX = 0, topY = 0, posX = 0, posY = 0;
			int antialiasing = Math.max(scene.getAntialiasingMinValue(), scene.getAntialiasingMaxValue());
			int dim =  (int)Math.pow(2, antialiasing);
			float weight = 1f / (dim * dim); 
			float delta = 1f / dim;
			Random random = new Random();
			for(y = from; y < to; y++){
				for(x = 0; x < width; x++){
					
					
					resultColor.set(Color.BLACK);
					topX = x - dim/2; 
					topY = y + dim/2;
					
					for(int i = 0;  i < dim; i++){
						for(int j=0; j < dim; j++){						
							posX = random.nextFloat()* delta + topX + delta*i;
							posY = random.nextFloat()* delta + topY + delta*j;
							ray = camera.getRay(posX, posY, width, height);                

							color.set(Color.BLACK);
							rayTrace(scene, ray, color, DEPTH);					
							resultColor.add(color);
						}					
					}
					
					resultColor.mul(weight);
					screenThreads.setRGB(x, height -1 - y, resultColor.toRGB());
					/*if (mustShowProgress() && ((height * x + y) % (n / 50) == 0)) 
						System.out.printf("#");*/
				}
				
			}
		}
		else
		{
			for(y = from; y < to; y++){
				for(x = 0; x < width; x++){
					/*if(x == 357 && y == 113)
						topX = 2; // PARA DEBUG*/
					ray = camera.getRay(x, y, width, height);                
					color.set(Color.BLACK);
					rayTrace(scene, ray, color, DEPTH);					
				
		
					screenThreads.setRGB(x, height -1 - y, color.toRGB());
					/*if (mustShowProgress() && ((height * x + y) % (n / 50) == 0)) 
						System.out.printf("#");*/
				}
				
			}
		}       

		if (mustShowProgress())
			System.out.printf("#");
		  
	}
	private int  movePointLights( List<Light> backUpLights, List<Light> sceneLights, int ant){
		int h=0;
		float x,y,z;
		//ya se desplazaron todas las luces 
		if(ant >= backUpLights.size())
			return ant;
		
		sceneLights.set(ant, ((PointLight)backUpLights.get(ant)));
		for (h=ant; h<backUpLights.size();h++){
			x=(float) Math.random() ; // 3 es el factor/ radio en el que se mueve la luz
			y= (float) Math.random() ;
			z= (float) Math.random() ;
			if (sceneLights.get(h) instanceof PointLight && ant <= h){
				sceneLights.set(h, ((PointLight)backUpLights.get(h)).moveLocation(x, y, z));
				return h;
			}
		}
		return h+1;
	}
	
	class GlobalCount{
		int gCount;
		public GlobalCount(){
			gCount = 0;
		}
		
		public int incrementGC(){
			return gCount++;
		}
	}
	
	private BufferedImage renderCoreThread(boolean doAntialiasing, int nThreads){
		
		Slave[] slaves= new Slave[nThreads];
		int width = scene.getWidth(); 
		int height = scene.getHeight(); 
		screenThreads = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);


		gc = new GlobalCount();
		
		Camera camera = scene.getCamera();
		camera.update();
		
		if (mustShowProgress()){
			System.out.println("|--------------------PROGRESS----------------------|");
			System.out.printf("|");
		}
		for(int i = 0; i < nThreads; i++){
			slaves[i]=new Slave(doAntialiasing, camera, width, height, scene.getLights());
			slaves[i].start();
		}

		for(int i = 0; i < nThreads; i++){
			try{
				slaves[i].join();
			}catch (Exception e) {
				//TODO: lanzar exception
			}
		}
		if(mustShowProgress())
			System.out.println("|  >>> COMPLETE!\n|--------------------------------------------------|");
		else
			System.out.println("COMPLETE!");
		
		return screenThreads;
		
	}
	
	class Slave extends Thread{
		int width, height;
		BufferedImage screen;
		boolean doAntialiasing;
		List<Light> lights;
		
		Camera camera;
		
		public Slave(boolean doAntialiasing, Camera camera, int width, int height,
				List<Light> lights){
			this.doAntialiasing= doAntialiasing;
			this.camera = camera;
			this.lights = lights;
			this.width = width;
			this.height = height;
		}
		


		@Override
		public void run() {
			int line;
			while(true){
				synchronized(gc){
					line = gc.incrementGC();
				}
				if(line < LINES)
					renderCoreFromTo(doAntialiasing, line, camera, width, height, lights);
				else
					break;
			}
		}

	}
	
	

	private Primitive doIntersection(Scene scene, Ray ray, IntersectionState intersection){
		IntersectionState currentIntersection = new IntersectionState(); 
		float minDistance = Float.MAX_VALUE; 
		float currentDistance;        
		//BoundingBox bounds = null;
		Primitive minDistancePrimitive = null; 
		Point3 rayOrigin = ray.getOrigin(); 
		
		/*uniGrid.intersect(ray, currentIntersection);
		if(currentIntersection.getQuantity() == 0){
			return null;
		}*/

		for (Primitive primitive : scene.getPrimitives()) { 
			/*bounds = primitive.getBounds();	
			
			if(bounds != null && !bounds.intersects(ray))
				continue;*/
			//currentIntersection.clearPoints();
			if(primitive.Intersect(ray, currentIntersection)) 
			{ 
				
				currentDistance = currentIntersection.getIntersectionPoint().distanceTo(rayOrigin); 

				if(currentDistance < minDistance){
					intersection.copyFromAnother(currentIntersection);
					minDistance = currentDistance; 
					minDistancePrimitive = currentIntersection.getPrimitive(); 
				}                 
			}                         
		}         	

		return minDistancePrimitive;
	}


	private boolean rayTrace(Scene scene, Ray ray, Color color, int depth) {         
	
		if(depth <= 0)
			return false;
	
		IntersectionState i = new IntersectionState(); 

		Primitive p = doIntersection(scene, ray, i);
	
		if(p == null)
			return false;

		if(p instanceof Light){
			color.set(((Light)p).getColor());
			return true;
		}
	
		SceneState state = new SceneState(this, scene, ray, color, p, i);
		float reflection = p.getReflectionK();
		float refraction =  p.getRefractionK();
	
		if(reflection > 0 && i.getIntersectionKind() == IntersectionKind.OUT){
			Color reflectionColor = new Color(Color.BLACK);
			Vector3 reflectionDir = state.getReflectedDirection();
			Point3 origin = Point3.add(i.getIntersectionPoint(),
					new Vector3(reflectionDir).mul(0.001f), new Point3());
			Ray reflectedRay = new Ray(origin, reflectionDir);        	
	
			if(rayTrace(scene, reflectedRay, reflectionColor, depth -1)){
				reflectionColor.mul(reflection);        		
			}
			state.setReflectionColor(reflectionColor);
		}       
	
		if(refraction > 0){        	
			Vector3 normal = new Vector3(state.getNormal()).mul(i.getIntersectionKind().coeficient);
			Vector3 directionOfRay = new Vector3(ray.getDirection());
			float n = 1.0f;
	
			if(i.getIntersectionKind() == IntersectionKind.OUT){
				n = 1.0f / p.getRefractionIndex();
			}
			else{
				n = p.getRefractionIndex();
			}
	
			float cosI = -Vector3.dot(normal, directionOfRay);
			float cosT2 = 1.0f - n * n * (1.0f - cosI * cosI);
	
			if(cosT2 > 0.0f){
				Color refractionColor = new Color(Color.BLACK);
				Vector3 directionOfRefracted = Vector3.add(directionOfRay.mul(n), 
						normal.mul((float)(n * cosI - Math.sqrt(cosT2))), new Vector3());
				directionOfRefracted.normalize();
	
				Point3 origin = Point3.add(i.getIntersectionPoint(),
						new Vector3(directionOfRefracted).mul(0.001f), new Point3());
				Ray reflectedRay = new Ray(origin, directionOfRefracted);    	
	
				if(rayTrace(scene, reflectedRay, refractionColor, depth -1)){
					refractionColor.mul(refraction);
					state.setRefractionColor(refractionColor);    				
				}
			}      
		}   
		
			
		ArrayList<Shader> shaders = p.getShaders();
		if(shaders != null){
			for (Shader shader : shaders) {
				shader.shade(state);
			}
		}
	
		if(reflection > 0){
			color.add(state.getReflectionColor());
		}
	
		if(refraction > 0){
			color.add(state.getRefractionColor());        	
		}
		
		
		return true;
	}
	

} 